package EP::Visualization::HeatmapGraphic;

use strict;

use EP::Config;
use EP::Visualization::Hooks;
use EP::Visualization::Graphics;

use GD;

use File::Temp qw /tempfile/;

use PDL;
use PDL::NiceSlice;

use PDL::Graphics::PGPLOT;
use PDL::Graphics::PGPLOT::Window;

use PGPLOT;

our @ISA = qw ( EP::Visualization::Graphics );

sub setData {
  my $self = shift;
  my %params = @_;

  die "No heatmap matrix supplied to HeatmapGraphic::setData" if not defined $params{data};

  $self->{data}        = $params{data};
  $self->{cell_height} = $params{cell_height} if $params{cell_height};
  $self->{cell_width}  = $params{cell_width}  if $params{cell_width};

  $self->{tmpdir}                = $params{tmpdir} if defined $params{tmpdir};
  $self->{tmpdir}                = $EP::Config::h->{TMP} if not defined $params{tmpdir};

  $self->{interactive} = $params{interactive} if     defined $params{interactive};
  $self->{interactive} = 0                    if not defined $params{interactive};
}

sub getWidth {
  my $self = shift;
  my $data = $self->getData();

  my ( $cols, $rows ) = $data->dims();

  return $cols * $self->{cell_width};
}

sub getHeight {
  my $self = shift;
  my $data = $self->getData();

  my ( $cols, $rows ) = $data->dims();

  return $rows * $self->{cell_height};
}

#####################################################
#returns string identifying the object as a heatmap #
#####################################################

sub getType{
  return "heatmap";
}

####################################################
#Set the vertical hooks (corresponds to the rows)  #
####################################################

sub setVerticalHookSet{
  my $self = shift;
  my $hook= shift;
  $self->{verticalHook} = $hook;
  return;
}
####################################################
#get the vertical hooks (corresponds to the rows)  #
####################################################

sub getVerticalHookSet{
  my $self = shift;
  return $self->{verticalHook};
}

#########################################################
#set the horizontal hooks (corresponds to the columns)  #
#########################################################

sub setHorizontalHookSet{
  my $self = shift;
  my $hook= shift;
  $self->{horizontalHook} = $hook;
  return;
}

#########################################################
#get the horizontal hooks (corresponds to the columns)  #
#########################################################

sub getHorizontalHookSet {
  my $self = shift;
  return $self->{horizontalHook};
}

sub to_png {
  my $self = shift;
  my ( $png, $height, $width, $xposition, $yposition ) = @_;

  $self->order();

  my $data = $self->getData();

  my $rect_w = $self->{cell_width};
  my $rect_h = $self->{cell_height};

  my $png_heatmap_fh = $self->draw_png_heatmap ( $data, $width, $height, $rect_w, $rect_h, 1 );
  my $gd_heatmap = GD::Image->newFromPng ( "$png_heatmap_fh" );

  # HACK - if failed, try again. Some weirdness.
  if ( not $gd_heatmap ) {
    $png_heatmap_fh = $self->draw_png_heatmap ( $data, $width, $height, $rect_w, $rect_h, 1 );
    $gd_heatmap = GD::Image->newFromPng ( "$png_heatmap_fh" );
  }

  if ( $width > 1000 ) {
    $png->copyResampled ( $gd_heatmap, $xposition, $yposition, 0, 0, $width, $height, 1000, $height ) if $gd_heatmap;
  } else {
    $png->copy ( $gd_heatmap, $xposition, $yposition, 0, 0, $width, $height ) if $gd_heatmap;
  }
#  unlink "$png_heatmap_fh";
  return $png;
}

##########################################################
#add the heatmap svg elements to the svg initiated in    #
#the Grid object                                         #
##########################################################

sub to_svg {
  my $self = shift;
  my ( $svg, $height, $width, $xpos, $ypos ) = @_;

  $self->order();    #order data before getting it!!

  my $data = $self->getData();
  my ( $cols, $rows ) = $data->dims();

  my $rect_w = $self->{cell_width};
  my $rect_h = $self->{cell_height};

  my $id = "ht" . int ( rand ( 100 ) );
  my $group = $svg->group ( id => $id, transform => "translate($xpos,$ypos)" );  #translates the heatmap according to it's position in the grid

  # just produce a PNG if too many rows/cols
  my $png = $self->draw_png_heatmap ( $data, $width, $height, $rect_w, $rect_h );
  $group->rectangle ( 'x'    => 0,
		      'y'    => 0,
		      width  => $width + 2,
		      height => $height + 2,
		      style => 'fill:gray' );

  $group->image ( 'x'    => 0,
		  'y'    => 0,
		  width  => $width + 2,
		  height => $height + 2,
		  "xlink:href"  => "$EP::Config::h->{TMP}/$png"
		);

  unless ( $self->{interactive} == 0 or $rows >= 500 or $cols >= 500 ) {
#  if ( $rows <= 20 ) {
    my $v_hooks = $self->getVerticalHookSet();
    my $v_hook_hooks = $v_hooks->getHookSet();
    my $v_hook_annot = $v_hooks->getAnnot();
    my @v_annot_names = keys %{$v_hook_annot->{$$v_hook_hooks[0]}->{full_annot}};

    my $f = $group->filter ( filterUnits => "userSpaceOnUse", id => "tt_fe_$id", x => "-10%", y => "-20%", width => "140%", height => "200%" );
    $f->feComponentTransfer ( in => "SourceAlpha" )->feFuncA ( type => "table", tableValues => "0 0.5" );
    $f->feGaussianBlur ( stdDeviation => 2 );
    $f->feOffset ( dx => 3, dy => 3, result => "shadow" );
    $f->feComposite ( in => "SourceGraphic", in2 => "shadow", operator => "over" );

    $group->rect ( id => "tt_bg_$id", fill => "#EFB", stroke => "black", x => 6, y => 6, height => 20, width => 0, filter => "url(#tt_fe_$id)", 'shape-rendering' => "crispEdges" );
    $group->text ( 'text-rendering' => 'optimizeLegibility', x => 9, 'y' => 17, 'font-size' => '14px', fill => 'black' )->tref ( id => "ht_gene_desc_$id", 'xlink:href' => '#' );

    my $scale_w = ( $width + 2 )  / ( $rect_w * $cols );
    my $scale_h = ( $height + 2 ) / ( $rect_h * $rows );

    my $g = $group->group ( transform => "scale($scale_w,$scale_h)" );
    $g->script ( 'a3:scriptImplementation' => "Adobe", type => "text/ecmascript" )->cdata ( <<__JS__ );
var t;
var tt_bg = document.getElementById ( "tt_bg_$id" );
var tt_tr = document.getElementById ( "ht_gene_desc_$id" );

function Over ( evt, w ) {
  if ( t == evt.target ) return;

  t = evt.target;

  t.setAttribute ( 'stroke', 'red' );
  t.setAttribute ( 'stroke-width', '1' );

  tt_bg.setAttribute ( 'width', w );
  tt_tr.setAttribute ( 'xlink:href', '#' + t.id );
}

function Out ( evt ) {
  if ( !t ) return;

  t.setAttribute ( 'stroke', '' );
  t.setAttribute ( 'stroke-width', '1' );
}
__JS__

    for my $i ( 0 .. ($rows - 1) ) {
      my $annot = $v_hook_annot->{$i}->{full_annot};
      my $desc = "Gene $i: $annot->{$v_annot_names[0]}";
      my $w = length ( $desc ) * 9;
      my $r = $g->rectangle ( id =>  "#$id" . "gene_desc_$i",  x => 0, y => $i * $rect_h, width => $width, height => $rect_h, fill => "none", 'pointer-events' => 'fill', onmouseover => "Over(evt, $w)", onmouseout => "Out(evt)" );
      $r->desc -> cdata ( $desc );
    }
  }

  return $svg;
}

sub draw_png_heatmap {
  my $self = shift;
  my ( $data, $width, $height, $rect_w, $rect_h, $for_to_png ) = @_;
  my ( $cols, $rows ) = $data->dims();

  my $png = new File::Temp ( DIR     => "$self->{tmpdir}",
			     UNLINK   => 0,
			     SUFFIX => '.png' );

  # HACK: it seems PGPLOT has a hard-coded limit of 1000 for image width. If image is needed bigger than 1000, we'll need to scale it.
  $width = 1000 if $width > 1000;

  $ENV{PGPLOT_BACKGROUND} = 'gray';

  my $win = PDL::Graphics::PGPLOT::Window->new ( Device => "$png/png", Size => [ $width, $height, 3], Axis => -2, HardCH => 0 );
#  $win->env ( -0.5, $cols - 0.5, $rows - 0.5, -0.5, {Axis => -2, HardCH => 0} );

  # set up levels:
  # $l: four levels - start 0, half-way 0.5, end 0.99, and tack-on: 1
  my $l = pdl 0, 0.5, 0.99, 1;

  # $r, $g, $b - red/green/blue components at each level:
  # 0,0,1 - blue; 0,0,0 - black; 1,1,0 - yellow; 190/255,190/255,190/255 - gray
  # so - all low values will be blue, all high values will be yellow, all middle-range values - around black, and all REALLY high values - gray. Set NA to really high -> NA as gray.
  my $r = pdl 0, 1, 1, 246/250;
  my $g = pdl 0, 1, 0, 1;
  my $b = pdl 1, 1, 0, 0;

  my ($mean,$rms,$median) = ($data->stats())[0,1,2];
  $rms = $mean / 10 if not $rms;

  $win->ctab ( $l, $r, $g, $b );
  warn "Should get here... rms = $rms";
  
  $win->imag ( $data(:,($rows-1):0)->setbadtoval(999999) , {Range => [$median-$rms*3,$median+$rms*3], Axis => -2, HardCH => 0 } );
  warn "...and here";
  
  $win->close ();

  $ENV{PGPLOT_BACKGROUND} = 'white';

#  close $png;
#  chmod 0444, $png;		# make readable for web-browsers

  warn "Generated heatmap into PNG ($width x $height): $png" if DEBUG;
  return $png if $for_to_png;

  my ($png_filename) = $png->filename() =~ /$EP::Config::h->{TMP}\/(.*)/;
  return $png_filename;
}

###########################################################
#set the graphics object by which the columns of the data #
#should be ordered to                                     #
###########################################################

sub orderHorizontalBy{
  my $self = shift;
  my $graphicsObj = shift;
  $self -> { orderHorizontalBy } = $graphicsObj;
  return;
}

###########################################################
#set the graphics object by which the rows of the data    #
#should be ordered to                                     #
###########################################################

sub orderVerticalBy{
  my $self = shift;
  my $graphicsObj = shift;
  $self -> { orderVerticalBy } = $graphicsObj;
  return;
}

###################################################################
#order the datamatrix according to the graphics objects set above #
###################################################################
sub order {
  my $self = shift;

  my ( $col_order, $row_order );
  my $data = $self->getData();

  # Horizontal ordering: columns of the heatmap
  if ( exists $self->{orderHorizontalBy} ) {
    my $graphicsObj = $self->{orderHorizontalBy};

    my @orderedHook = @{$graphicsObj->getHook()->getHookSet()};
    if ( $graphicsObj -> getOrientation() == 180){
      @orderedHook = reverse @orderedHook;
    }

    $col_order = long @orderedHook;

    my $hook = new EP::Visualization::Hooks( \@orderedHook );
    $self->setHorizontalHookSet( $hook );
  }

  # Vertical ordering: rows of the heatmap
  if ( exists $self->{orderVerticalBy} ) {
    my $graphicsObj = $self->{orderVerticalBy};

    my @orderedHook = @{ $graphicsObj -> getHook -> getHookSet() };
    if ( $graphicsObj -> getOrientation() == -90 ){
      @orderedHook = reverse @orderedHook;
    }

    $row_order = long @orderedHook;

    my $hook = new EP::Visualization::Hooks( \@orderedHook );
    $self -> setVerticalHookSet( $hook );
  }

  my $ordered_data;

  $ordered_data = $data($col_order,$row_order) if defined $col_order and defined $row_order;
  $ordered_data = $data(,$row_order) if not defined $col_order and defined $row_order;
  $ordered_data = $data($col_order,) if defined $col_order and not defined $row_order;
  $ordered_data = $data if not defined $col_order and not defined $row_order;

  $self->setData ( data => $ordered_data, cell_height => $self->{cell_height}, cell_width => $self->{cell_width}, interactive => $self->{interactive} );
}

1;
